
import { Renderer, Camera, Geometry, Program, Texture, Mesh, Vec2 } from '../../index';
import { GPGPU } from '../../';

const vertex = /* glsl */ `
            precision highp float;

            attribute vec2 coords;
            attribute vec4 random;

            uniform float uTime;
            uniform sampler2D tPosition;
            uniform sampler2D tVelocity;

            varying vec4 vRandom;
            varying vec4 vVelocity;

            void main() {
                vRandom = random;

                // Get position from texture, rather than attribute
                vec4 position = texture2D(tPosition, coords);
                vVelocity = texture2D(tVelocity, coords);
                
                // Add some subtle random oscillating so it never fully stops
                position.xy += sin(vec2(uTime) * vRandom.wy + vRandom.xz * 6.28) * vRandom.zy * 0.1;

                gl_Position = vec4(position.xy, 0, 1);
                gl_PointSize = mix(2.0, 15.0, vRandom.x);

                // Make bigger while moving
                gl_PointSize *= 1.0 + min(1.0, length(vVelocity.xy));

            }
        `;

const fragment = /* glsl */ `
            precision highp float;

            varying vec4 vRandom;
            varying vec4 vVelocity;

            void main() {

                // Circle shape
                if (step(0.5, length(gl_PointCoord.xy - 0.5)) > 0.0) discard;

                // Random colour
                vec3 color = vec3(vRandom.zy, 1.0) * mix(0.7, 2.0, vRandom.w);

                // Fade to white when not moving, with an ease off curve
                gl_FragColor.rgb = mix(vec3(1), color, 1.0 - pow(1.0 - smoothstep(0.0, 0.7, length(vVelocity.xy)), 2.0));

                gl_FragColor.a = 1.0;
            }
        `;

const positionFragment = /* glsl */ `
            precision highp float;

            uniform float uTime;
            uniform sampler2D tVelocity;

            // Default texture uniform for GPGPU pass is 'tMap'.
            // Can use the textureUniform parameter to update.
            uniform sampler2D tMap;

            varying vec2 vUv;

            void main() {
                vec4 position = texture2D(tMap, vUv);
                vec4 velocity = texture2D(tVelocity, vUv);

                position.xy += velocity.xy * 0.01;
                                
                // Keep in bounds
                vec2 limits = vec2(1);
                position.xy += (1.0 - step(-limits.xy, position.xy)) * limits.xy * 2.0;
                position.xy -= step(limits.xy, position.xy) * limits.xy * 2.0;

                gl_FragColor = position;
            }
        `;

const velocityFragment = /* glsl */ `
            precision highp float;

            uniform float uTime;
            uniform sampler2D tPosition;
            uniform sampler2D tMap;
            uniform vec2 uMouse;

            varying vec2 vUv;

            void main() {
                vec4 position = texture2D(tPosition, vUv);
                vec4 velocity = texture2D(tMap, vUv);

                // Repulsion from mouse
                vec2 toMouse = position.xy - uMouse;
                float strength = smoothstep(0.3, 0.0, length(toMouse));
                velocity.xy += strength * normalize(toMouse) * 0.5;

                // Friction
                velocity.xy *= 0.98;

                gl_FragColor = velocity;
            }
        `;


const renderer = new Renderer({ dpr: 2 });
const gl = renderer.gl;
document.body.appendChild(gl.canvas);
gl.clearColor(1, 1, 1, 1);

const camera = new Camera(gl, { fov: 45 });
camera.position.set(0, 0, 5);

function resize() {
    renderer.setSize(window.innerWidth, window.innerHeight);
    camera.perspective({ aspect: gl.canvas.width / gl.canvas.height });
}
window.addEventListener('resize', resize, false);
resize();

// Common uniforms
const time = { value: 0 };
const mouse = { value: new Vec2() };

// The number of particles will determine how large the GPGPU textures are,
// and therefore how expensive the GPU calculations will be.
// Below I'm using 65536 to use every pixel of a 256x256 texture. If I used one more (65537), 
// it would need to use a 512x512 texture - the GPU would then perform calculations for each pixel,
// meaning that nearly 3/4 of the texture (196607 pixels) would be redundant.
const numParticles = 65536;

// Create the initial data arrays for position and velocity. 4 values for RGBA channels in texture.
const initialPositionData = new Float32Array(numParticles * 4);
const initialVelocityData = new Float32Array(numParticles * 4);

// Random to be used as regular static attribute
const random = new Float32Array(numParticles * 4);
for (let i = 0; i < numParticles; i++) {
    initialPositionData.set([
        (Math.random() - 0.5) * 2.0,
        (Math.random() - 0.5) * 2.0,
        0, // the Green and Alpha channels go unused in this example, however I set
        1, // unused Alpha to 1 so that texture is visible in WebGL debuggers
    ], i * 4);
    initialVelocityData.set([0, 0, 0, 1], i * 4);
    random.set([
        Math.random(),
        Math.random(),
        Math.random(),
        Math.random(),
    ], i * 4);
}

// Initialise the GPGPU classes, creating the FBOs and corresponding texture coordinates
const position = new GPGPU(gl, { data: initialPositionData });
const velocity = new GPGPU(gl, { data: initialVelocityData });

// Add the simulation shaders as passes to each GPGPU class
position.addPass({
    fragment: positionFragment,
    uniforms: {
        uTime: time,
        tVelocity: velocity.uniform,
    },
});
velocity.addPass({
    fragment: velocityFragment,
    uniforms: {
        uTime: time,
        uMouse: mouse,
        tPosition: position.uniform,
    },
});

// Now we can create our geometry, using the coordinates from above.
// We don't use the velocity or position data as attributes, 
// instead we will get this from the FBO textures in the shader.
const geometry = new Geometry(gl, {
    random: { size: 4, data: random },

    // Could use either position or velocity coords, as they are the same
    coords: { size: 2, data: position.coords },
});

const program = new Program(gl, {
    vertex,
    fragment,
    uniforms: {
        uTime: time,
        tPosition: position.uniform,
        tVelocity: velocity.uniform,
    },
});

const points = new Mesh(gl, { geometry, program, mode: gl.POINTS });

// Add handlers to get mouse position
const isTouchCapable = 'ontouchstart' in window;
if (isTouchCapable) {
    window.addEventListener('touchstart', updateMouse, false);
    window.addEventListener('touchmove', updateMouse, false);
} else {
    window.addEventListener('mousemove', updateMouse, false);
}

function updateMouse(e) {
    if (e.changedTouches && e.changedTouches.length) {
        e.x = e.changedTouches[0].pageX;
        e.y = e.changedTouches[0].pageY;
    }
    if (e.x === undefined) {
        e.x = e.pageX;
        e.y = e.pageY;
    }

    // Get mouse value in -1 to 1 range, with y flipped
    mouse.value.set(
        (e.x / gl.renderer.width) * 2 - 1,
        (1.0 - e.y / gl.renderer.height) * 2 - 1
    );
}

requestAnimationFrame(update);
function update(t) {
    requestAnimationFrame(update);

    time.value = t * 0.001;

    // Update the GPGPU classes
    velocity.render();
    position.render();

    renderer.render({ scene: points, camera });
}

document.getElementsByClassName('Info')[0].innerHTML = 'GPGPU Particles (General-Purpose computing on Graphics Processing Units)';
document.title = 'OGL • GPGPU Particles (General-Purpose computing on Graphics Processing Units)';
